// SMSLib for Java v3
// A Java API library for sending and receiving SMS via a GSM modem
// or other supported gateways.
// Web Site: http://www.smslib.org
//
// Copyright (C) 2002-2012, Thanasis Delenikas, Athens/GREECE.
// SMSLib is distributed under the terms of the Apache License version 2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package org.smslib.smsserver;

import java.io.FileInputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;
import org.smslib.ICallNotification;
import org.smslib.IInboundMessageNotification;
import org.smslib.IOrphanedMessageNotification;
import org.smslib.IOutboundMessageNotification;
import org.smslib.IQueueSendingNotification;
import org.smslib.InboundMessage;
import org.smslib.InboundMessage.MessageClasses;
import org.smslib.Library;
import org.smslib.Message.MessageTypes;
import org.smslib.OutboundMessage;
import org.smslib.Service;
import org.smslib.helper.Logger;
import org.smslib.smsserver.gateways.AGateway;
import org.smslib.smsserver.interfaces.Interface;
import org.smslib.smsserver.interfaces.Interface.InterfaceTypes;

/**
 * SMSServer Application.
 */
public class SMSServer {
	Properties props;

	boolean shutdown = false;

	List<Interface<? extends Object>> infList;

	InboundNotification inboundNotification;

	OutboundNotification outboundNotification;

	CallNotification callNotification;

	QueueSendingNotification queueSendingNotification;

	OrphanedMessageNotification orphanedMessageNotification;

	InboundPollingThread inboundPollingThread;

	OutboundPollingThread outboundPollingThread;

	boolean optRunOnce = false;

	public SMSServer() {
		this.infList = new ArrayList<Interface<? extends Object>>();
		Runtime.getRuntime().addShutdownHook(new Shutdown());
		this.inboundNotification = new InboundNotification();
		this.outboundNotification = new OutboundNotification();
		this.callNotification = new CallNotification();
		this.queueSendingNotification = new QueueSendingNotification();
		orphanedMessageNotification = new OrphanedMessageNotification();
		this.inboundPollingThread = null;
		this.outboundPollingThread = null;
		// Service.getInstance().setInboundMessageNotification(this.inboundNotification);
		Service.getInstance().setOutboundMessageNotification(
				this.outboundNotification);
		Service.getInstance().setCallNotification(this.callNotification);
		Service.getInstance().setQueueSendingNotification(
				this.queueSendingNotification);
		Service.getInstance().setOrphanedMessageNotification(
				this.orphanedMessageNotification);
	}

	public List<Interface<? extends Object>> getInfList() {
		return this.infList;
	}

	public boolean getShutdown() {
		return shutdown;
	}

	public Properties getProperties() {
		return this.props;
	}

	class Shutdown extends Thread {
		@Override
		public void run() {
			Logger.getInstance().logInfo(
					"SMSServer shutting down, please wait...", null, null);
			SMSServer.this.shutdown = true;
			try {
				stopInterfaces();
				if (Service.getInstance().getQueueManager() != null)
					Service.getInstance().getQueueManager()
							.removeAllPendingMessages();
				if (Service.getInstance().getQueueManager() != null)
					Service.getInstance().getQueueManager()
							.removeAllDelayedMessages();
				Service.getInstance().stopService();
				if (SMSServer.this.inboundPollingThread != null) {
					SMSServer.this.inboundPollingThread.interrupt();
					SMSServer.this.inboundPollingThread.join();
				}
				if (SMSServer.this.outboundPollingThread != null) {
					SMSServer.this.outboundPollingThread.interrupt();
					SMSServer.this.outboundPollingThread.join();
				}
			} catch (Exception e) {
				Logger.getInstance().logError("Shutdown hook error.", e, null);
				e.printStackTrace();
			}
		}
	}

	@SuppressWarnings("unchecked")
	private void loadConfiguration() throws Exception {
		FileInputStream f = null;
		try {
			this.props = new Properties();
			if (System.getProperty("smsserver.configdir") != null)
				f = new FileInputStream(
						System.getProperty("smsserver.configdir")
								+ "SMSServer.conf");
			else if (System.getProperty("smsserver.configfile") != null)
				f = new FileInputStream(
						System.getProperty("smsserver.configfile"));
			else
				f = new FileInputStream("SMSServer.conf");
			getProperties().load(f);
			if (getProperties().getProperty("smsserver.balancer", "").length() > 0) {
				try {
					Class<?> c = Class
							.forName((getProperties().getProperty(
									"smsserver.balancer", "").indexOf('.') == -1 ? "org.smslib.balancing."
									: "")
									+ getProperties().getProperty(
											"smsserver.balancer", ""));
					Constructor<?> constructor = c.getConstructor();
					org.smslib.balancing.LoadBalancer balancer = (org.smslib.balancing.LoadBalancer) constructor
							.newInstance();
					Service.getInstance().setLoadBalancer(balancer);
					Logger.getInstance().logInfo(
							"SMSServer: set balancer to: "
									+ getProperties().getProperty(
											"smsserver.balancer", ""), null,
							null);
				} catch (Exception e) {
					e.printStackTrace();
					Logger.getInstance().logError(
							"SMSServer: error setting custom balancer!", null,
							null);
				}
			}
			if (getProperties().getProperty("smsserver.router", "").length() > 0) {
				try {
					Class<?> c = Class
							.forName((getProperties().getProperty(
									"smsserver.router", "").indexOf('.') == -1 ? "org.smslib.routing."
									: "")
									+ getProperties().getProperty(
											"smsserver.router", ""));
					Constructor<?> constructor = c.getConstructor();
					org.smslib.routing.Router router = (org.smslib.routing.Router) constructor
							.newInstance();
					Service.getInstance().setRouter(router);
					Logger.getInstance()
							.logInfo(
									"SMSServer: set router to: "
											+ getProperties().getProperty(
													"smsserver.router", ""),
									null, null);
				} catch (Exception e) {
					Logger.getInstance().logError(
							"SMSServer: error setting custom balancer!", null,
							null);
				}
			}
			for (int i = 0; i < Integer.MAX_VALUE; i++) {
				try {
					String propName = "gateway." + i;
					String propValue = getProperties()
							.getProperty(propName, "");
					if (propValue.length() == 0)
						break;
					StringTokenizer tokens = new StringTokenizer(propValue, ",");
					String gtwId = tokens.nextToken().trim();
					String gtwClass = tokens.nextToken().trim();
					Object[] args = new Object[] { gtwId, getProperties(), this };
					Class<?>[] argsClass = new Class[] { String.class,
							Properties.class, SMSServer.class };
					Class<?> c = Class
							.forName((gtwClass.indexOf('.') == -1 ? "org.smslib.smsserver.gateways."
									: "")
									+ gtwClass);
					Constructor<?> constructor = c.getConstructor(argsClass);
					AGateway gtw = (AGateway) constructor.newInstance(args);
					gtw.create();
					Service.getInstance().addGateway(gtw.getGateway());
					Logger.getInstance().logInfo(
							"SMSServer: added gateway " + gtwId + " / "
									+ gtw.getDescription(), null, null);
				} catch (Exception e) {
					Logger.getInstance()
							.logError(
									"SMSServer: Unknown Gateway in configuration file!",
									null, null);
				}
			}
			for (int i = 0; i < Integer.MAX_VALUE; i++) {
				try {
					String propName = "interface." + i;
					String propValue = getProperties()
							.getProperty(propName, "");
					if (propValue.length() == 0)
						break;
					StringTokenizer tokens = new StringTokenizer(propValue, ",");
					String infId = tokens.nextToken().trim();
					String infClass = tokens.nextToken().trim();
					InterfaceTypes infType = null;
					String sinfType = tokens.hasMoreTokens() ? tokens
							.nextToken() : "inoutbound";
					sinfType = sinfType.trim();
					if ("inbound".equalsIgnoreCase(sinfType)) {
						infType = InterfaceTypes.INBOUND;
					} else if ("outbound".equalsIgnoreCase(sinfType)) {
						infType = InterfaceTypes.OUTBOUND;
					} else { // NULL or other crap
						infType = InterfaceTypes.INOUTBOUND;
					}
					Object[] args = new Object[] { infId, getProperties(),
							this, infType };
					Class<?>[] argsClass = new Class[] { String.class,
							Properties.class, SMSServer.class,
							InterfaceTypes.class };
					Class<?> c = Class
							.forName((infClass.indexOf('.') == -1 ? "org.smslib.smsserver.interfaces."
									: "")
									+ infClass);
					Constructor<?> constructor = c.getConstructor(argsClass);
					Interface<? extends Object> inf = (Interface<? extends Object>) constructor
							.newInstance(args);
					getInfList().add(inf);
					Logger.getInstance().logInfo(
							"SMSServer: added interface " + infId + " / "
									+ inf.getDescription() + " / "
									+ inf.getType(), null, null);
				}
				/* Check for exceptions thrown by the constructor itself */
				catch (InvocationTargetException e) {
					Logger.getInstance().logError(
							"SMSServer: Illegal Interface configuration: "
									+ e.getCause().getMessage(), null, null);
				} catch (Exception e) {
					Logger.getInstance()
							.logError(
									"SMSServer: Unknown Interface in configuration file!",
									null, null);
				}
			}
		} finally {
			if (f != null)
				f.close();
		}
	}

	class InboundPollingThread extends Thread {
		@Override
		public void run() {
			try {
				while (!SMSServer.this.shutdown) {
					Logger.getInstance().logDebug(
							"InboundPollingThread() run.", null, null);
					readMessages();
					if (SMSServer.this.optRunOnce) {
						SMSServer.this.shutdown = true;
						new Shutdown().start();
						break;
					}
					Thread.sleep(Integer.parseInt(getProperties().getProperty(
							"settings.inbound_interval", "60")) * 1000);
				}
			} catch (InterruptedException e) {
				Logger.getInstance().logDebug(
						"InboundPollingThread() interrupted.", null, null);
			} catch (Exception e) {
				Logger.getInstance().logDebug(
						"Error in InboundPollingThread()", e, null);
			}
		}
	}

	class OutboundPollingThread extends Thread {
		@Override
		public void run() {
			try {
				while (!SMSServer.this.shutdown) {
					Logger.getInstance().logDebug(
							"OutboundPollingThread() run.", null, null);
					sendMessages();
					if (SMSServer.this.optRunOnce)
						break;
					Thread.sleep(Integer.parseInt(getProperties().getProperty(
							"settings.outbound_interval", "60")) * 1000);
				}
			} catch (InterruptedException e) {
				Logger.getInstance().logDebug(
						"OutboundPollingThread() interrupted.", null, null);
			} catch (Exception e) {
				Logger.getInstance().logDebug(
						"Error in OutboundPollingThread()", e, null);
			}
		}
	}

	private void process() throws Exception {
		this.inboundPollingThread = new InboundPollingThread();
		this.inboundPollingThread.setName("SMSServer - InboundPollingThread");
		this.inboundPollingThread.start();
		this.outboundPollingThread = new OutboundPollingThread();
		this.outboundPollingThread.setName("SMSServer - OutboundPollingThread");
		this.outboundPollingThread.start();
		while (!this.shutdown)
			Thread.sleep(1000);
	}

	void startInterfaces() throws Exception {
		for (Interface<? extends Object> inf : getInfList())
			inf.start();
	}

	void stopInterfaces() throws Exception {
		for (Interface<? extends Object> inf : getInfList())
			inf.stop();
	}

	private void run() throws Exception {
		loadConfiguration();
		try {
			startInterfaces();
			Service.getInstance().startService();
			process();
		} catch (Exception e) {
			Logger.getInstance().logError("SMSServer error!", e, null);
			stopInterfaces();
			Service.getInstance().stopService();
			if (this.inboundPollingThread != null) {
				this.inboundPollingThread.interrupt();
				this.inboundPollingThread.join();
			}
			if (this.outboundPollingThread != null) {
				this.outboundPollingThread.interrupt();
				this.outboundPollingThread.join();
			}
		}
	}

	void readMessages() {
		List<InboundMessage> msgList = new ArrayList<InboundMessage>();
		try {
			Service.getInstance().readMessages(msgList, MessageClasses.ALL);
			if (msgList.size() > 0) {
				for (Interface<? extends Object> inf : getInfList())
					if (inf.isInbound())
						inf.messagesReceived(msgList);
				if (getProperties().getProperty(
						"settings.delete_after_processing", "no")
						.equalsIgnoreCase("yes"))
					for (InboundMessage msg : msgList)
						Service.getInstance().deleteMessage(msg);
			}
		} catch (Exception e) {
			Logger.getInstance().logError(
					"SMSServer: reading messages exception!", e, null);
		}
	}

	void sendMessages() {
		boolean foundOutboundGateway = false;
		for (org.smslib.AGateway gtw : Service.getInstance().getGateways())
			if (gtw.isOutbound()) {
				foundOutboundGateway = true;
				break;
			}
		if (foundOutboundGateway) {
			List<OutboundMessage> msgList = new ArrayList<OutboundMessage>();
			try {
				for (Interface<? extends Object> inf : getInfList())
					if (inf.isOutbound())
						msgList.addAll(inf.getMessagesToSend());
			} catch (Exception e) {
				Logger.getInstance().logError(
						"SMSServer: sending messages exception!", e, null);
			}
			if (getProperties().getProperty("settings.send_mode", "sync")
					.equalsIgnoreCase(("sync"))) {
				Logger.getInstance().logInfo(
						"SMSServer: sending synchronously...", null, null);
				for (OutboundMessage msg : msgList) {
					try {
						Service.getInstance().sendMessage(msg);
						for (Interface<? extends Object> inf : getInfList())
							if (inf.isOutbound())
								inf.markMessage(msg);
					} catch (Exception e) {
						Logger.getInstance().logError(
								"SMSServer: sending messages exception!", e,
								null);
						try {
							for (Interface<? extends Object> inf : getInfList())
								if (inf.isOutbound())
									inf.markMessage(msg);
						} catch (Exception e1) {
							Logger.getInstance().logError(
									"SMSServer: sending messages exception!",
									e1, null);
						}
					}
				}
			} else {
				Logger.getInstance().logInfo(
						"SMSServer: sending asynchronously... ["
								+ msgList.size() + "]", null, null);
				for (OutboundMessage msg : msgList) {
					if (!Service.getInstance().queueMessage(msg)) {
						try {
							for (Interface<? extends Object> inf : getInfList())
								if (inf.isOutbound())
									inf.markMessage(msg);
						} catch (Exception e) {
							Logger.getInstance().logError(
									"SMSServer: sending messages exception!",
									e, null);
						}
					}
				}
			}
		}
	}

	class InboundNotification implements IInboundMessageNotification {
		@Override
		public void process(org.smslib.AGateway gateway, MessageTypes msgType,
				InboundMessage msg) {
			List<InboundMessage> msgList = new ArrayList<InboundMessage>();
			msgList.add(msg);
			for (Interface<? extends Object> inf : getInfList())
				if (inf.isInbound()) {
					try {
						inf.messagesReceived(msgList);
					} catch (Exception e) {
						Logger.getInstance().logError(
								"Error receiving message!", e, null);
					}
				}
			if (getProperties().getProperty("settings.delete_after_processing",
					"no").equalsIgnoreCase("yes")) {
				try {
					Service.getInstance().deleteMessage(msg);
				} catch (Exception e) {
					Logger.getInstance().logError(
							"Error deleting received message!", e, null);
				}
			}
			msgList.clear();
		}
	}

	class OutboundNotification implements IOutboundMessageNotification {
		@Override
		public void process(org.smslib.AGateway gateway,
				org.smslib.OutboundMessage msg) {
			try {
				for (Interface<? extends Object> inf : getInfList())
					if (inf.isOutbound())
						inf.markMessage(msg);
			} catch (Exception e) {
				Logger.getInstance().logError(
						"IOutboundMessageNotification error.", e, null);
			}
		}
	}

	class CallNotification implements ICallNotification {
		@Override
		public void process(org.smslib.AGateway gateway, String callerId) {
			try {
				for (Interface<? extends Object> inf : getInfList())
					inf.callReceived(gateway.getGatewayId(), callerId);
			} catch (Exception e) {
				Logger.getInstance().logError("ICallNotification error.", e,
						null);
			}
		}
	}

	class QueueSendingNotification implements IQueueSendingNotification {
		@Override
		public void process(org.smslib.AGateway gateway, OutboundMessage msg) {
			Logger.getInstance().logInfo(
					"**** >>>> Now Sending: " + msg.getRecipient(), null,
					gateway.getGatewayId());
		}
	}

	class OrphanedMessageNotification implements IOrphanedMessageNotification {
		@Override
		public boolean process(org.smslib.AGateway gateway, InboundMessage msg) {
			System.out
					.println("&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&");
			System.out
					.println("&&&&&&&&&&&&&&&&& ORPHANED INFO &&&&&&&&&&&&&&&&&");
			System.out
					.println("&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&");
			System.out.println(msg);
			System.out
					.println("&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&");
			System.out
					.println("&&&&&&&&&&&&&&&&& ORPHANED INFO &&&&&&&&&&&&&&&&&");
			System.out
					.println("&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&");
			// Return FALSE to leave orphaned parts in memory.
			return false;
		}
	}

	public boolean checkPriorityTimeFrame(int priority) {
		String timeFrame;
		String from, to, current;
		Calendar cal = Calendar.getInstance();
		if (priority < 0)
			timeFrame = getProperties().getProperty("settings.timeframe.low",
					"0000-2359");
		else if (priority == 0)
			timeFrame = getProperties().getProperty(
					"settings.timeframe.normal", "0000-2359");
		else if (priority >= 0)
			timeFrame = getProperties().getProperty("settings.timeframe.high",
					"0000-2359");
		else
			timeFrame = "0000-2359";
		from = timeFrame.substring(0, 4);
		to = timeFrame.substring(5, 9);
		cal.setTime(new java.util.Date());
		current = cal.get(Calendar.HOUR_OF_DAY) < 10 ? "0"
				+ cal.get(Calendar.HOUR_OF_DAY) : ""
				+ cal.get(Calendar.HOUR_OF_DAY);
		current += cal.get(Calendar.MINUTE) < 10 ? "0"
				+ cal.get(Calendar.MINUTE) : "" + cal.get(Calendar.MINUTE);
		if ((Integer.parseInt(current) >= Integer.parseInt(from))
				&& (Integer.parseInt(current) < Integer.parseInt(to)))
			return true;
		return false;
	}

	public static void main(String[] args) {
		System.out.println(Library.getLibraryDescription());
		System.out.println("\nSMSLib API version: "
				+ Library.getLibraryVersion());
		System.out.println("SMSServer version: " + Library.getLibraryVersion());
		SMSServer app = new SMSServer();
		for (int i = 0; i < args.length; i++) {
			if (args[i].equalsIgnoreCase("-runonce"))
				app.optRunOnce = true;
			else
				System.out.println("Invalid argument: " + args[i]);
		}
		try {
			app.run();
			Logger.getInstance().logInfo("SMSServer exiting normally.", null,
					null);
		} catch (Exception e) {
			Logger.getInstance().logError("SMSServer Error: ", e, null);
			try {
				Service.getInstance().stopService();
			} catch (Exception e1) {
				Logger.getInstance().logError(
						"SMSServer error while shutting down: ", e1, null);
			}
		}
	}
}
